package code_generator

import (
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"os"
	"regexp"
	"strings"

	"gitee.com/nggs/tools/print"
	"gitee.com/nggs/util"
)

var noGenRegexp = regexp.MustCompile(`\s*!gen?\s*`)

func MustAnalyzeDir(dir string) *PackageInfo {
	pkgName, fileSet, files := parseFiles(dir)
	pkgAst, _ := ast.NewPackage(fileSet, files, nil, nil)
	pkgDoc := doc.New(pkgAst, pkgName, doc.AllDecls)

	var pkgInfo = &PackageInfo{
		FileSet:    fileSet,
		Dir:        dir,
		Name:       pkgName,
		ConstTypes: make(map[string]string),
		FileMap:    make(map[string]*FileInfo),
	}
	analyzeConstTypes(pkgInfo, pkgDoc)
	analyzeStructs(pkgInfo, pkgDoc)
	return pkgInfo
}

func parseFiles(dir string) (pkgName string, fileSet *token.FileSet, fileMap map[string]*ast.File) {
	fileSet = token.NewFileSet()
	fileMap = make(map[string]*ast.File)

	err := util.Walk(dir, "go", nil, nil, func(path string, info os.FileInfo, name string, isInBlackList bool) bool {
		if strings.HasSuffix(name, ".ex") {
			// 跳过*.ex.go
			return true
		}

		print.Debugf("<- %s", path)

		file, err := parser.ParseFile(fileSet, path, nil, parser.ParseComments)
		if err != nil {
			print.Panicf("parser.ParseFile %s fail, %s", path, err)
		}

		fileMap[name] = file

		pkgName = file.Name.String()
		return true
	})
	if err != nil {
		print.Panicf("parseFiles fail, %s", err)
	}
	return
}

// find 'type XXX int'
func analyzeConstTypes(pkgInfo *PackageInfo, pkgDoc *doc.Package) {
	for _, t := range pkgDoc.Types {
		if len(t.Consts) == 0 {
			continue
		}
		typeSpec, ok := t.Decl.Specs[0].(*ast.TypeSpec)
		if !ok {
			continue
		}
		declName := typeSpec.Name.String()
		typeName := typeSpec.Type.(*ast.Ident).Name
		pkgInfo.ConstTypes[declName] = typeName
		print.Debugf("\tType '%s' -> '%s'", declName, typeName)
	}
}

func analyzeStructs(pkgInfo *PackageInfo, pkgDoc *doc.Package) {
	for _, t := range pkgDoc.Types {
		matches := noGenRegexp.FindStringSubmatch(t.Doc)
		if len(matches) > 0 {
			print.Debugf("ignore %s", t.Name)
			continue
		}

		typeSpec, ok := t.Decl.Specs[0].(*ast.TypeSpec)
		if !ok {
			continue
		}
		structType, ok := typeSpec.Type.(*ast.StructType)
		if !ok {
			continue
		}

		structInfo := analyzeStruct(pkgInfo, t.Name, structType)
		file := pkgInfo.File(t.Decl.Pos())
		file.Structs[t.Name] = structInfo
	}
}

func analyzeStruct(pkgInfo *PackageInfo, structName string, structType *ast.StructType) *StructInfo {
	si := &StructInfo{Name: structName}
	for _, field := range structType.Fields.List {
		typeInfo := analyzeType(pkgInfo, field.Type)
		isFixLen := analyzeFixLen(typeInfo)
		for _, name := range field.Names {
			si.Fields = append(si.Fields, &FieldInfo{
				Name:     name.String(),
				Type:     typeInfo,
				IsFixLen: isFixLen,
			})
		}
	}
	return si
}

func analyzeFixLen(t *TypeInfo) bool {
	if t.IsArray {
		if t.Len == "" {
			return false
		}
		return analyzeFixLen(t.Type)
	}
	return t.Size != 0
}

func analyzeType(pkgInfo *PackageInfo, astType ast.Expr) *TypeInfo {
	var ti TypeInfo
	switch t := astType.(type) {
	case *ast.StarExpr:
		ti.IsPoint = true
		ti.Type = analyzeType(pkgInfo, t.X)

	case *ast.ArrayType:
		if size, ok := t.Len.(*ast.BasicLit); ok {
			ti.Len = size.Value
		}
		if t, ok := t.Elt.(*ast.Ident); ok {
			realName := t.Name
			if realName2, ok := pkgInfo.ConstTypes[t.Name]; ok {
				realName = realName2
				if realName == "byte" {
					ti.DefName = t.Name
				}
			}
			if realName == "byte" {
				ti.Name = "[]byte"
				break
			}
		}
		ti.IsArray = true
		ti.Type = analyzeType(pkgInfo, t.Elt)

	case *ast.MapType:

	case *ast.Ident:
		ti.Name = t.Name
		ti.DefName = ti.Name
		if realName, ok := pkgInfo.ConstTypes[t.Name]; ok {
			ti.DefName = ti.Name
			ti.Name = realName
		}
		switch ti.Name {
		case "string":
			ti.Size = 0
		case "int8", "uint8", "byte", "bool":
			ti.Size = 1
		case "int16", "uint16":
			ti.Size = 2
		case "int32", "uint32", "float32":
			ti.Size = 4
		case "int", "uint", "int64", "uint64", "float64":
			ti.Size = 8
		default:
			ti.IsUnknown = true
		}

	default:
		print.Panicf("Unsupported field type %#v", astType)
	}
	return &ti
}
